<?php
error_reporting(E_ALL ^ E_NOTICE);
ob_implicit_flush();
  
//地址与接口，即创建socket时需要服务器的IP和端口
$sk=new Sock('127.0.0.1',8000);

//对创建的socket循环进行监听，处理数据
$sk->run();
  
//下面是sock类
class Sock{
  public $sockets; //socket的连接池，即client连接进来的socket标志
  public $users;  //所有client连接进来的信息，包括socket、client名字等
  public $master; //socket的resource，即前期初始化socket时返回的socket资源
    
  private $sda=array();  //已接收的数据
  private $slen=array(); //数据总长度
  private $sjen=array(); //接收数据的长度
  private $ar=array();  //加密key
  private $n=array();
    
  public function __construct($address, $port){
  
    //创建socket并把保存socket资源在$this->master
    $this->master=$this->WebSocket($address, $port);
  
    //创建socket连接池
    $this->sockets=array($this->master);
  }
    
  //对创建的socket循环进行监听，处理数据
  function run(){
    //死循环，直到socket断开
    $flag = true;
    socket_close($this->master);
    while($flag){
      $changes=$this->sockets;
      $write=NULL;
      $except=NULL;
        
      /*
      //这个函数是同时接受多个连接的关键，我的理解它是为了阻塞程序继续往下执行。
      socket_select ($sockets, $write = NULL, $except = NULL, NULL);
  
      $sockets可以理解为一个数组，这个数组中存放的是文件描述符。当它有变化（就是有新消息到或者有客户端连接/断开）时，socket_select函数才会返回，继续往下执行。
      $write是监听是否有客户端写数据，传入NULL是不关心是否有写变化。
      $except是$sockets里面要被排除的元素，传入NULL是”监听”全部。
      最后一个参数是超时时间
      如果为0：则立即结束
      如果为n>1: 则最多在n秒后结束，如遇某一个连接有新动态，则提前返回
      如果为null：如遇某一个连接有新动态，则返回
      */
      socket_select($changes,$write,$except,NULL);
      foreach($changes as $sock){
          
        //如果有新的client连接进来，则
        if($sock==$this->master){
  
          //接受一个socket连接
          $client=socket_accept($this->master);
  
          //给新连接进来的socket一个唯一的ID
          $key=uniqid();
          $this->sockets[]=$client; //将新连接进来的socket存进连接池
          $this->users[$key]=array(
            'socket'=>$client, //记录新连接进来client的socket信息
            'shou'=>false    //标志该socket资源没有完成握手
          );
        //否则1.为client断开socket连接，2.client发送信息
        }else{
          $len=0;
          $buffer='';
          //读取该socket的信息，注意：第二个参数是引用传参即接收数据，第三个参数是接收数据的长度
          do{
            $l=socket_recv($sock,$buf,1000,0);
            $len+=$l;
            $buffer.=$buf;
          }while($l == 1000);
          //根据socket在user池里面查找相应的$k,即健ID
          $k=$this->search($sock);
  
          //如果接收的信息长度小于7，则该client的socket为断开连接
          if($len<7){
            //给该client的socket进行断开操作，并在$this->sockets和$this->users里面进行删除
            $this->send2($k);
            continue;
          }
          //判断该socket是否已经握手
          if(!$this->users[$k]['shou']){
            //如果没有握手，则进行握手处理
            $this->woshou($k,$buffer);
          }else{
            //走到这里就是该client发送信息了，对接受到的信息进行uncode处理
            $buffer = $this->uncode($buffer,$k);
            if($buffer==false){
              continue;
            }
            //如果不为空，则进行消息推送操作
            $this->send($k,$buffer);
          }
        }
      }
        
    }

  }
    
  //指定关闭$k对应的socket
  function close($k){
    //断开相应socket
    socket_close($this->users[$k]['socket']);
    //删除相应的user信息
    unset($this->users[$k]);
    //重新定义sockets连接池
    $this->sockets=array($this->master);
    foreach($this->users as $v){
      $this->sockets[]=$v['socket'];
    }
    //输出日志
    $this->e("key:$k close");
  }
    
  //根据sock在users里面查找相应的$k
  function search($sock){
    foreach ($this->users as $k=>$v){
      if($sock==$v['socket'])
      return $k;
    }
    return false;
  }
    
  //传相应的IP与端口进行创建socket操作
  function WebSocket($address,$port){
    $server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
    socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);//1表示接受所有的数据包
    socket_bind($server, $address, $port);
    socket_listen($server);
    $this->e('Server Started : '.date('Y-m-d H:i:s'));
    $this->e('Listening on  : '.$address.' port '.$port);
    return $server;
  }
    
    
  /*
  * 函数说明：对client的请求进行回应，即握手操作
  * @$k clien的socket对应的健，即每个用户有唯一$k并对应socket
  * @$buffer 接收client请求的所有信息
  */
  function woshou($k,$buffer){
  
    //截取Sec-WebSocket-Key的值并加密，其中$key后面的一部分258EAFA5-E914-47DA-95CA-C5AB0DC85B11字符串应该是固定的
    $buf = substr($buffer,strpos($buffer,'Sec-WebSocket-Key:')+18);
    $key = trim(substr($buf,0,strpos($buf,"\r\n")));
    $new_key = base64_encode(sha1($key."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true));
      
    //按照协议组合信息进行返回
    $new_message = "HTTP/1.1 101 Switching Protocols\r\n";
    $new_message .= "Upgrade: websocket\r\n";
    $new_message .= "Sec-WebSocket-Version: 13\r\n";
    $new_message .= "Connection: Upgrade\r\n";
    $new_message .= "Sec-WebSocket-Accept: " . $new_key . "\r\n\r\n";
    socket_write($this->users[$k]['socket'],$new_message,strlen($new_message));
  
    //对已经握手的client做标志
    $this->users[$k]['shou']=true;
    return true;
      
  }
    
  //解码函数
  function uncode($str,$key){
    $mask = array(); 
    $data = ''; 
    $msg = unpack('H*',$str);
    $head = substr($msg[1],0,2); 
    if ($head == '81' && !isset($this->slen[$key])) { 
      $len=substr($msg[1],2,2);
      $len=hexdec($len);//把十六进制的转换为十进制
      if(substr($msg[1],2,2)=='fe'){
        $len=substr($msg[1],4,4);
        $len=hexdec($len);
        $msg[1]=substr($msg[1],4);
      }else if(substr($msg[1],2,2)=='ff'){
        $len=substr($msg[1],4,16);
        $len=hexdec($len);
        $msg[1]=substr($msg[1],16);
      }
      $mask[] = hexdec(substr($msg[1],4,2)); 
      $mask[] = hexdec(substr($msg[1],6,2)); 
      $mask[] = hexdec(substr($msg[1],8,2)); 
      $mask[] = hexdec(substr($msg[1],10,2));
      $s = 12;
      $n=0;
    }else if($this->slen[$key] > 0){
      $len=$this->slen[$key];
      $mask=$this->ar[$key];
      $n=$this->n[$key];
      $s = 0;
    }
      
    $e = strlen($msg[1])-2;
    for ($i=$s; $i<= $e; $i+= 2) { 
      $data .= chr($mask[$n%4]^hexdec(substr($msg[1],$i,2))); 
      $n++; 
    } 
    $dlen=strlen($data);
      
    if($len > 255 && $len > $dlen+intval($this->sjen[$key])){
      $this->ar[$key]=$mask;
      $this->slen[$key]=$len;
      $this->sjen[$key]=$dlen+intval($this->sjen[$key]);
      $this->sda[$key]=$this->sda[$key].$data;
      $this->n[$key]=$n;
      return false;
    }else{
      unset($this->ar[$key],$this->slen[$key],$this->sjen[$key],$this->n[$key]);
      $data=$this->sda[$key].$data;
      unset($this->sda[$key]);
      return $data;
    }
      
  }
    
  //与uncode相对
  function code($msg){
    $frame = array(); 
    $frame[0] = '81'; 
    $len = strlen($msg);
    if($len < 126){
      $frame[1] = $len<16?'0'.dechex($len):dechex($len);
    }else if($len < 65025){
      $s=dechex($len);
      $frame[1]='7e'.str_repeat('0',4-strlen($s)).$s;
    }else{
      $s=dechex($len);
      $frame[1]='7f'.str_repeat('0',16-strlen($s)).$s;
    }
    $frame[2] = $this->ord_hex($msg);
    $data = implode('',$frame); 
    return pack("H*", $data); 
  }
    
  function ord_hex($data) { 
    $msg = ''; 
    $l = strlen($data); 
    for ($i= 0; $i<$l; $i++) { 
      $msg .= dechex(ord($data{$i})); 
    } 
    return $msg; 
  }
    
  //用户加入或client发送信息
  function send($k,$msg){
    //将查询字符串解析到第二个参数变量中，以数组的形式保存如：parse_str("name=Bill&age=60",$arr)
    parse_str($msg,$g);
    $ar=array();
  
    if($g['type']=='add'){
      //第一次进入添加聊天名字，把姓名保存在相应的users里面
      $this->users[$k]['name']=$g['ming'];
      $ar['type']='add';
      $ar['name']=$g['ming'];
      $key='all';
    }else{
      //发送信息行为，其中$g['key']表示面对大家还是个人，是前段传过来的信息
      $ar['nrong']=$g['nr'];
      $key=$g['key'];
    }
    //推送信息
    $this->send1($k,$ar,$key);
  }
    
  //对新加入的client推送已经在线的client
  function getusers(){
    $ar=array();
    foreach($this->users as $k=>$v){
      $ar[]=array('code'=>$k,'name'=>$v['name']);
    }
    return $ar;
  }
    
  //$k 发信息人的socketID $key接受人的 socketID ，根据这个socketID可以查找相应的client进行消息推送，即指定client进行发送
  function send1($k,$ar,$key='all'){
    $ar['code1']=$key;
    $ar['code']=$k;
    $ar['time']=date('m-d H:i:s');
    //对发送信息进行编码处理
    $str = $this->code(json_encode($ar));
    //面对大家即所有在线者发送信息
    if($key=='all'){
      $users=$this->users;
      //如果是add表示新加的client
      if($ar['type']=='add'){
        $ar['type']='madd';
        $ar['users']=$this->getusers();    //取出所有在线者，用于显示在在线用户列表中
        $str1 = $this->code(json_encode($ar)); //单独对新client进行编码处理，数据不一样
        //对新client自己单独发送，因为有些数据是不一样的
        socket_write($users[$k]['socket'],$str1,strlen($str1));
        //上面已经对client自己单独发送的，后面就无需再次发送，故unset
        unset($users[$k]);
      }
      //除了新client外，对其他client进行发送信息。数据量大时，就要考虑延时等问题了
      foreach($users as $v){
        socket_write($v['socket'],$str,strlen($str));
      }
    }else{
      //单独对个人发送信息，即双方聊天
      socket_write($this->users[$k]['socket'],$str,strlen($str));
      socket_write($this->users[$key]['socket'],$str,strlen($str));
    }
  }
    
  //用户退出向所用client推送信息
  function send2($k){
    $this->close($k);
    $ar['type']='rmove';
    $ar['nrong']=$k;
    $this->send1(false,$ar,'all');
  }
    
  //记录日志
  function e($str){
    //$path=dirname(__FILE__).'/log.txt';
    $str=$str."\n";
    //error_log($str,3,$path);
    //编码处理
    echo iconv('utf-8','gbk//IGNORE',$str);
  }
}